/**
 * Copyright © 2008, EMC Corporation.
 * Redistribution and use in source and binary forms, with or without modification, 
 * are permitted provided that the following conditions are met:
 *
 *     + Redistributions of source code must retain the above copyright notice, 
 *       this list of conditions and the following disclaimer.
 *     + Redistributions in binary form must reproduce the above copyright 
 *       notice, this list of conditions and the following disclaimer in the 
 *       documentation and/or other materials provided with the distribution.
 *     + The name of EMC Corporation may not be used to endorse or promote 
 *       products derived from this software without specific prior written 
 *       permission.
 *
 *      THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 
 *      "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED 
 *      TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 
 *      PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS 
 *      BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR 
 *      CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF 
 *      SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS 
 *      INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
 *      CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) 
 *      ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE 
 *      POSSIBILITY OF SUCH DAMAGE.
 */
package com.emc.esu.helpers {
	import com.emc.esu.api.Checksum;
	import com.emc.esu.api.EsuApi;
	import com.emc.esu.api.EsuCallbackState;
	import com.emc.esu.api.EsuError;
	import com.emc.esu.api.Extent;
	import com.emc.esu.api.Identifier;
	import com.emc.esu.api.Metadata;
	import com.emc.esu.api.MetadataList;
	
	import flash.events.ErrorEvent;
	import flash.events.Event;
	import flash.events.EventDispatcher;
	import flash.events.ProgressEvent;
	import flash.utils.ByteArray;
	import flash.utils.IDataOutput;
	
	import mx.logging.ILogger;
	import mx.logging.Log;
	/**
	 * Dispatched when the download helper has completed
	 * @eventType flash.events.Event.COMPLETE
	 */
	[Event(name="complete", type="flash.events.Event")]
	/**
	 * Dispatched when the download helper encounters an error
	 * @eventType flash.events.ErrorEvent.ERROR
	 */
	[Event(name="error", type="flash.events.ErrorEvent")]
	/**
	 * Dispatched as data chunks are read from the server
	 * @eventType flash.events.ProgressEvent.PROGRESS
	 */
	[Event(name="progress", type="flash.events.ProgressEvent")]
	/**
	 * Helper class to download objects.  For large transfers, the content
	 * generally needs to be transferred from the server in smaller chunks.  This
	 * helper class reads an object's contents incrementally from the server and
	 * writes it to a stream.
	 */
	public class DownloadHelper extends EventDispatcher {
		private const DEFAULT_BUFFSIZE:int = 4096 * 1024; // 4MB

	    private var _buffer:ByteArray;
	    private var _requestSize:int;
	    private var _esu:EsuApi;
	    private var _stream:IDataOutput;
	    private var _bytesLoaded:Number;
	    private var _bytesTotal:Number;
	    private var _complete:Boolean;
	    private var _failed:Boolean;
	    private var _error:Error;
	    private var _objectId:Identifier;
	    private var _running:Boolean;
	    private var _userState:Object;
	    private var _checksumming:Boolean;
	    private var _checksum:Checksum;
	    
	    private var logger:ILogger;
	    
	    [Bindable]
	    /**
	     * The number of bytes loaded from the object
	     */
	    public function get bytesLoaded():Number {
	    	return _bytesLoaded;
	    }
	    public function set bytesLoaded( val:Number ):void {
	    	this._bytesLoaded = val;
	    }
	    
	    [Bindable]
	    /**
	     * The total number of bytes in the object
	     */
	    public function get bytesTotal():Number {
	    	return this._bytesTotal;
	    }
	   	public function set bytesTotal( val:Number ):void {
	   		this._bytesTotal = val;
	   	}
	   	
	   	[Bindable]
	   	/**
	   	 * Set to true when the request has completed
	   	 */
	   	public function get complete():Boolean {
	   		return this._complete;
	   	}
	   	public function set complete( val:Boolean ):void {
	   		this._complete = val;
	   	}
	   	
	   	[Bindable]
	   	/**
	   	 * Set to true when the request has failed
	   	 */
	   	public function get failed():Boolean {
	   		return this._failed;
	   	}
	   	public function set failed( val:Boolean ):void {
	   		this._failed = val;
	   	}
	   	
	   	[Bindable]
	   	/**
	   	 * Contains the error object if the request
	   	 * has failed, otherwise it will be null.
	   	 */
	   	public function get error():Error {
	   		return this._error;
	   	}
	   	public function set error( val:Error ):void {
	   		this._error = val;
	   	}

		[Bindable]
		/**
		 * Contains the ObjectId being downloaded.
		 */
		public function get objectId():Identifier {
			return this._objectId;
		}
		public function set objectId( val:Identifier ):void {
			this._objectId = val;
		}
		
		[Bindable]
		/**
		 * Allows the user to attach a state object to track information
		 * across events.
		 */
		public function get userState():Object {
			return this._userState;
		}
		public function set userState( val:Object ):void {
			this._userState = val;
		}
		
		public function get stream():IDataOutput {
			return this._stream;
		}
		
		[Bindable]
		public function get checksumming():Boolean {
			return this._checksumming;
		}
		public function set checksumming( val:Boolean ):void {
			this._checksumming = val;
		}
		
		public function get checksum():Checksum {
			return this._checksum;
		}
		
		/**
		 * Creates a new download helper.
		 * @param esu the EsuApi object to use for downloads.
		 * @param requestSize the amount of data to download in each request.  By
		 * default this is 4MB.
		 */
		public function DownloadHelper( esu:EsuApi, requestSize:Number = DEFAULT_BUFFSIZE ) {
			this.logger = Log.getLogger("EsuApi");
			this._esu = esu;
			this._requestSize = requestSize;
			this._buffer = new ByteArray();
		}

		/**
		 * Starts the download of the Object.
		 * @param id the ObjectId to download.
		 * @param stream the stream to write the object to.
		 */
    	public function readObject( id:Identifier, stream:IDataOutput ):void {
    		this._objectId = id;
    		this._stream = stream;
    		this.bytesLoaded = 0;
    		this.failed = false;
    		this.error = null;
    		this.complete = false;
	        this._running = true;
	        
	        if( this._checksumming ) {
	        	this._checksum = new Checksum( Checksum.ALGORITHM_SHA0 );
	        }
    		
    		// Fetch the object size from the server
    		this._esu.getSystemMetadata( id, null, handleSystemMetadata, null ); 
    	}
    	
    	/**
    	 * Handles the system metadata response.  Parses out the object size and
    	 * starts the download
    	 */
    	private function handleSystemMetadata( state:EsuCallbackState ):void { 
    		if( !state.wasSuccessful() ) {
    			fail( state.getError() );
    			return;
    		}
    		
    		var mlist:MetadataList = MetadataList( state.getResult() );
    		if( mlist.getMetadata( "size" ) == null ) {
    			fail( new EsuError( "Could not get size for " + this._objectId ) );
    			return;
    		} 
    		
    		var m:Metadata = mlist.getMetadata( "size" );
    		var sz:String = m.getValue();
    		
    		if( sz != null && sz.length > 0 ) {
    			this.bytesTotal = Number( sz );
    		} else {
    			fail( new EsuError( "Could not get size for " + this._objectId ) );
    			return;    			
    		}
    		
    		downloadChunk();
    	}
    	
    	/**
    	 * Downloads thenext chunk from the server.
    	 */
    	private function downloadChunk():void {
    		if( !this._running ) {
    			fail( new EsuError( "Download canceled" ) );
    		}
    		
    		if( this._bytesLoaded >= this._bytesTotal ) {
    			// Done
    			done();
    			return;
    		}
    		
            var extent:Extent = null;

            // Determine how much data to download.  If we're at the last
            // request in the transfer, only request as many bytes as needed
            // to get to the end of the file.
            if ( this._bytesLoaded + this._requestSize > this._bytesTotal ) {
                // Would go past end of file.  Request less bytes.                                      
                extent = new Extent( this._bytesLoaded, this._bytesTotal
                        - this._bytesLoaded );
            } else {
                extent = new Extent( this._bytesLoaded, this._requestSize );
            }
           	this._buffer.length = extent.getSize();
           	this._buffer.position = 0;
           	
           	this._esu.readObject( this._objectId, extent, this._buffer, 
           		handleReadObject, extent.getSize(), _checksum );
    		
    	}
    	
    	/**
    	 * Processes the read object response, writes it to the stream
    	 * and fetches the next chunk
    	 * @param state the callback state from the read object request.
    	 */
    	private function handleReadObject( state:EsuCallbackState ):void {
    		if( !state.wasSuccessful() ) {
    			fail( state.getError() );
    			return;
    		}
    		
    		if( !this._running ) {
    			fail( new EsuError( "Download canceled" ) );
    			return;
    		}
    		
    		var ba:ByteArray = ByteArray( state.getResult() );
    		var size:Number = Number( state.getStateObject() );
    		
    		if( ba.length != size ) {
    			fail( new EsuError( "Did not receive number of bytes requested (" + 
    				ba.length + " != " + size + ")" ) );
    			return;
    		}
    		
    		// Write
    		this._stream.writeBytes( ba, 0, ba.length );
    		
    		progress( ba.length );
    		
    		downloadChunk();
    	}

		/**
		 * Cancels the download.
		 */
		public function cancel():void {
			logger.debug( "Canceling download" );
			this._running = false;
		}

		/**
		 * Processes download progress and dispatches progress events.
		 */
        private function progress( bytes:int ):void {
        	this.bytesLoaded += bytes;
        	var event:ProgressEvent = new ProgressEvent( ProgressEvent.PROGRESS );
        	event.bytesLoaded = this._bytesLoaded;
        	event.bytesTotal = this._bytesTotal;

        	this.dispatchEvent( event );
        }
        
        /**
         * Processes errors and dispatches error events.
         */
        private function fail( e:Error ):void {
        	this.failed = true;
        	this.error = e;
        	
        	var event:ErrorEvent = new ErrorEvent( ErrorEvent.ERROR, false, false, e.message );
        	this.dispatchEvent( event );
        }

		/**
		 * Completes the download and dispatches the complete event.
		 */
		private function done():void {
			this.complete = true;
			
			var event:Event = new Event( Event.COMPLETE );
			this.dispatchEvent( event );
		}

	}
}